Skip to content

feat: add per-operation auth descriptor with precedence ladder#155

Merged
OmarAlJarrah merged 2 commits into
mainfrom
feat/auth-descriptor-precedence
Jun 28, 2026
Merged

feat: add per-operation auth descriptor with precedence ladder#155
OmarAlJarrah merged 2 commits into
mainfrom
feat/auth-descriptor-precedence

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Jun 17, 2026

Copy link
Copy Markdown
Member

Auth requirements are per-operation, and a two-boolean "needs-auth / needs-key" model does not generalise to operations that accept several alternative schemes with different OAuth parameters. This adds a hand-constructable, scheme-agnostic auth descriptor model to sdk-core plus a deterministic resolver.

What this adds (org.dexpace.sdk.core.http.auth)

  • AuthRequirement — one accepted AuthScheme paired with its own OAuth scopes/params. Where AuthMetadata flattens an operations schemes into a single shared OAuth bag, a requirement pairs each scheme with its own parameters. Immutable, private-field copy-in + Builder/newBuilder, of(scheme) convenience.
  • AuthDescriptor — the per-operation ordered list of AuthRequirements in preference order. Immutable, Builder/newBuilder, of(...) / ofSchemes(...) factories, allowsAnonymous(). Records which schemes are acceptable and in what order, never how a scheme is stamped onto the wire.
  • AuthDescriptorTier — the precedence tier an descriptor occupies: PER_CALL > OPERATION > CLIENT.
  • AuthResolution — the resolved outcome: the chosen requirement, the tier it came from, and an isAnonymous flag.
  • AuthResolutionException — tailored failure naming the required and available schemes (e.g. operation requires one of [OAUTH2, API_KEY] but no matching credential is available (have: [BASIC])).
  • AuthDescriptorResolver — the precedence ladder. Two independent orders:
    1. Tier precedence: the most specific present descriptor wins outright (per-call override > operation default > client default). A supplied higher tier does not fall through to a lower one if it cannot be satisfied — an explicit per-call override fails rather than silently degrading.
    2. Requirement precedence: within the chosen descriptor, requirements are tried in declared order and the first satisfiable scheme wins. NO_AUTH is always satisfiable (anonymous access); any other scheme is satisfiable iff it is in the caller-supplied available-schemes set.

Scheme-agnostic by design

Core never inspects a concrete Credential or knows how a scheme is stamped. The caller supplies the set of schemes it can satisfy (derived from configured credentials) and maps the resolved requirement to a credential + auth step itself. Per-cloud / OAuth specifics stay in adapters. There is no code generation — these are the runtime primitives a generator would later target, fully usable by hand today.

Tests

New suites cover requirement/descriptor construction + builders + defensive copies, tier precedence (including the no-fall-through rule), requirement precedence, anonymous (NO_AUTH) handling, OAuth-parameter forwarding, and the tailored failure message.

Gated build (scoped, run locally)

./gradlew :sdk-core:test :sdk-core:ktlintCheck :sdk-core:detekt --no-daemon   # BUILD SUCCESSFUL
./gradlew :sdk-core:apiDump --no-daemon                                       # regenerated, committed
./gradlew :sdk-core:apiCheck --no-daemon                                      # BUILD SUCCESSFUL

Closes #63

Revision — consolidated onto one model

  • Removed AuthMetadata. It flattened an operation's schemes into a single shared OAuth bag, was never wired into a pipeline path, and is fully superseded by AuthRequirement / AuthDescriptor (each scheme paired with its own OAuth parameters). Keeping both would have left two parallel models with no bridge. Its test is removed, and the AuthScheme KDoc and README package map now reference the descriptor model.
  • AuthRequirement construction now matches AuthDescriptor — the primary constructor is private, with an of(scheme, scopes, params) factory (@JvmStatic / @JvmOverloads) alongside the Builder, so the two sibling value types share one construction pattern.
  • AuthRequirement.Builder's OAuth setters defensively copy their argument, so a source collection mutated between the setter call and build() cannot leak into the built instance.

:sdk-core test / ktlintCheck / detekt / apiCheck all pass; the API snapshot is regenerated.

Auth requirements are per-operation, and a two-boolean "needs-auth /
needs-key" model does not generalise to operations that accept several
alternative schemes with different OAuth parameters. This adds a
hand-constructable, scheme-agnostic descriptor model to sdk-core plus a
deterministic resolver.

- AuthRequirement: one accepted AuthScheme paired with its own OAuth
  scopes/params (immutable, Builder + newBuilder).
- AuthDescriptor: a per-operation ordered list of AuthRequirements in
  preference order (immutable, Builder + newBuilder, of/ofSchemes
  factories). Records which schemes are acceptable, never how they are
  stamped onto the wire.
- AuthDescriptorTier / AuthResolution: the precedence tier and the
  resolved outcome (requirement + tier + anonymous flag).
- AuthDescriptorResolver: applies two precedence orders — tier
  precedence (per-call override > operation default > client default,
  no fall-through past a supplied higher tier) then requirement
  precedence within the chosen descriptor (first satisfiable scheme
  wins; NO_AUTH is always satisfiable). Throws AuthResolutionException
  naming the required and available schemes when nothing matches.

The resolver stays scheme-agnostic: callers supply the set of schemes
they can satisfy and map the resolved requirement to a concrete
credential and auth step themselves; per-cloud / OAuth specifics stay
in adapters. No code generation — these are the runtime primitives a
generator would later target.

Closes #63
Collapse the auth-requirement model onto a single type. AuthMetadata
flattened an operation's schemes into one shared OAuth bag and was never
wired into any pipeline path; AuthDescriptor/AuthRequirement supersede it
with per-scheme OAuth parameters, so remove AuthMetadata and its test.

Align AuthRequirement construction with AuthDescriptor: make the primary
constructor private and expose an of(scheme, scopes, params) factory
(@JvmStatic/@jvmoverloads) alongside the Builder, so the two sibling value
types share one construction pattern.

Defensively copy the OAuth scopes/params in AuthRequirement.Builder's
setters so a source collection mutated between the setter call and build()
cannot leak into the built instance, matching the copy-in discipline used
elsewhere in the type.

Update AuthScheme KDoc and the README package map to reference the
descriptor model; regenerate the sdk-core API snapshot.
@OmarAlJarrah OmarAlJarrah merged commit c001993 into main Jun 28, 2026
1 check passed
@OmarAlJarrah OmarAlJarrah deleted the feat/auth-descriptor-precedence branch June 28, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate per-operation auth descriptors with a precedence ladder

1 participant